/*
 * Modified by Qin Zhang, originally from Ant source code.
 * Modifications:
 *   (1) class package and class name changed.
 *   (2) one method is commented out.
 *   (3) class access right changed from public to package private.
 */

/*
 * Copyright  2002-2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package org.hackystat.sensorbase.uripattern;

import java.io.File;
import java.util.StringTokenizer;
import java.util.Vector;


//import org.apache.tools.ant.types.Resource;

/**
 * <p><b>For package internal use only. Copied from Ant 1.6.</b></p>
 *
 * <p>This is a utility class used by selectors and DirectoryScanner. The
 * functionality more properly belongs just to selectors, but unfortunately
 * DirectoryScanner exposed these as protected methods. Thus we have to
 * support any subclasses of DirectoryScanner that may access these methods.
 * </p>
 * <p>This is a Singleton.</p>
 *
 * @author Arnout J. Kuiper
 * <a href="mailto:ajkuiper@wxs.nl">ajkuiper@wxs.nl</a>
 * @author Magesh Umasankar
 * @author <a href="mailto:bruce@callenish.com">Bruce Atherton</a>
 * @since 1.5
 *
 * @version Ant 1.6
 */
final class PatternMatcherImpl {

  private static PatternMatcherImpl instance = new PatternMatcherImpl();

  /**
   * Private Constructor.
   */
  private PatternMatcherImpl() {
  }

  /**
   * Retrieves the instance of the Singleton.
   * @return singleton instance
   */
  public static PatternMatcherImpl getInstance() {
    return instance;
  }
  
  private static String separator = "/";
  private static char separatorChar = '/';

  /**
   * Tests whether or not a given path matches the start of a given
   * pattern up to the first "**".
   * <p>
   * This is not a general purpose test and should only be used if you
   * can live with false positives. For example, <code>pattern=**\a</code>
   * and <code>str=b</code> will yield <code>true</code>.
   *
   * @param pattern The pattern to match against. Must not be
   *                <code>null</code>.
   * @param str     The path to match, as a String. Must not be
   *                <code>null</code>.
   *
   * @return whether or not a given path matches the start of a given
   * pattern up to the first "**".
   */
  public static boolean matchPatternStart(String pattern, String str) {
    return matchPatternStart(pattern, str, true);
  }

  /**
   * Tests whether or not a given path matches the start of a given
   * pattern up to the first "**".
   * <p>
   * This is not a general purpose test and should only be used if you
   * can live with false positives. For example, <code>pattern=**\a</code>
   * and <code>str=b</code> will yield <code>true</code>.
   *
   * @param pattern The pattern to match against. Must not be
   *                <code>null</code>.
   * @param str     The path to match, as a String. Must not be
   *                <code>null</code>.
   * @param isCaseSensitive Whether or not matching should be performed
   *                        case sensitively.
   *
   * @return whether or not a given path matches the start of a given
   * pattern up to the first "**".
   */
  public static boolean matchPatternStart(String pattern, String str,
                                          boolean isCaseSensitive) {
    // When str starts with a File.separator, pattern has to start with a
    // File.separator.
    // When pattern starts with a File.separator, str has to start with a
    // File.separator.
    if (str.startsWith(separator)
        != pattern.startsWith(separator)) {
      return false;
    }

    String[] patDirs = tokenizePathAsArray(pattern);
    String[] strDirs = tokenizePathAsArray(str);

    int patIdxStart = 0;
    int patIdxEnd = patDirs.length - 1;
    int strIdxStart = 0;
    int strIdxEnd = strDirs.length - 1;

    // up to first '**'
    while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
      String patDir = patDirs[patIdxStart];
      if ("**".equals(patDir)) {
        break;
      }
      if (!match(patDir, strDirs[strIdxStart], isCaseSensitive)) {
        return false;
      }
      patIdxStart++;
      strIdxStart++;
    }


//    if (strIdxStart > strIdxEnd) {
//      // String is exhausted
//      return true;
//    }
//    else if (patIdxStart > patIdxEnd) {
//      // String not exhausted, but pattern is. Failure.
//      return false;
//    }
//    else {
//      // pattern now holds ** while string is not exhausted
//      // this will generate false positives but we can live with that.
//      return true;
//    }

    //check style is too stupid, compains, modify to please it.
    boolean theResult;
    if (strIdxStart > strIdxEnd) {
      theResult = true;
    }
    else if (patIdxStart > patIdxEnd) {
      theResult = false;
    }
    else {
      theResult = true;
    }
    return theResult;
  }

  /**
   * Tests whether or not a given path matches a given pattern.
   *
   * @param pattern The pattern to match against. Must not be
   *                <code>null</code>.
   * @param str     The path to match, as a String. Must not be
   *                <code>null</code>.
   *
   * @return <code>true</code> if the pattern matches against the string,
   *         or <code>false</code> otherwise.
   */
  public static boolean matchPath(String pattern, String str) {
    return matchPath(pattern, str, true);
  }

  /**
   * Tests whether or not a given path matches a given pattern.
   *
   * @param pattern The pattern to match against. Must not be
   *                <code>null</code>.
   * @param str     The path to match, as a String. Must not be
   *                <code>null</code>.
   * @param isCaseSensitive Whether or not matching should be performed
   *                        case sensitively.
   *
   * @return <code>true</code> if the pattern matches against the string,
   *         or <code>false</code> otherwise.
   */
  public static boolean matchPath(String pattern, String str, boolean isCaseSensitive) {
    // I DON'T THINK THIS APPLIES TO HACKYSTAT 8 (pmj 6/2007)
    // When str starts with a File.separator, pattern has to start with a
    // File.separator.
    // When pattern starts with a File.separator, str has to start with a
    // File.separator.
    //    if (str.startsWith(File.separator)
    //        != pattern.startsWith(File.separator)) {
    //      return false;
    //    }

    String[] patDirs = tokenizePathAsArray(pattern);
    String[] strDirs = tokenizePathAsArray(str);

    int patIdxStart = 0;
    int patIdxEnd = patDirs.length - 1;
    int strIdxStart = 0;
    int strIdxEnd = strDirs.length - 1;

    // up to first '**'
    while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
      String patDir = patDirs[patIdxStart];
      if ("**".equals(patDir)) {
        break;
      }
      if (!match(patDir, strDirs[strIdxStart], isCaseSensitive)) {
        patDirs = null;
        strDirs = null;
        return false;
      }
      patIdxStart++;
      strIdxStart++;
    }
    if (strIdxStart > strIdxEnd) {
      // String is exhausted
      for (int i = patIdxStart; i <= patIdxEnd; i++) {
        if (!patDirs[i].equals("**")) {
          patDirs = null;
          strDirs = null;
          return false;
        }
      }
      return true;
    }
    else {
      if (patIdxStart > patIdxEnd) {
        // String not exhausted, but pattern is. Failure.
        patDirs = null;
        strDirs = null;
        return false;
      }
    }

    // up to last '**'
    while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
      String patDir = patDirs[patIdxEnd];
      if ("**".equals(patDir)) {
        break;
      }
      if (!match(patDir, strDirs[strIdxEnd], isCaseSensitive)) {
        patDirs = null;
        strDirs = null;
        return false;
      }
      patIdxEnd--;
      strIdxEnd--;
    }
    if (strIdxStart > strIdxEnd) {
      // String is exhausted
      for (int i = patIdxStart; i <= patIdxEnd; i++) {
        if (!patDirs[i].equals("**")) {
          patDirs = null;
          strDirs = null;
          return false;
        }
      }
      return true;
    }

    while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
      int patIdxTmp = -1;
      for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
        if (patDirs[i].equals("**")) {
          patIdxTmp = i;
          break;
        }
      }
      if (patIdxTmp == patIdxStart + 1) {
        // '**/**' situation, so skip one
        patIdxStart++;
        continue;
      }
      // Find the pattern between padIdxStart & padIdxTmp in str between
      // strIdxStart & strIdxEnd
      int patLength = (patIdxTmp - patIdxStart - 1);
      int strLength = (strIdxEnd - strIdxStart + 1);
      int foundIdx = -1;
      strLoop:
          for (int i = 0; i <= strLength - patLength; i++) {
        for (int j = 0; j < patLength; j++) {
          String subPat = patDirs[patIdxStart + j + 1];
          String subStr = strDirs[strIdxStart + i + j];
          if (!match(subPat, subStr, isCaseSensitive)) {
            continue strLoop;
          }
        }

        foundIdx = strIdxStart + i;
        break;
      }

      if (foundIdx == -1) {
        patDirs = null;
        strDirs = null;
        return false;
      }

      patIdxStart = patIdxTmp;
      strIdxStart = foundIdx + patLength;
    }

    for (int i = patIdxStart; i <= patIdxEnd; i++) {
      if (!patDirs[i].equals("**")) {
        patDirs = null;
        strDirs = null;
        return false;
      }
    }

    return true;
  }

  /**
   * Tests whether or not a string matches against a pattern.
   * The pattern may contain two special characters:<br>
   * '*' means zero or more characters<br>
   * '?' means one and only one character
   *
   * @param pattern The pattern to match against.
   *                Must not be <code>null</code>.
   * @param str     The string which must be matched against the pattern.
   *                Must not be <code>null</code>.
   *
   * @return <code>true</code> if the string matches against the pattern,
   *         or <code>false</code> otherwise.
   */
  public static boolean match(String pattern, String str) {
    return match(pattern, str, true);
  }

  /**
   * Tests whether or not a string matches against a pattern.
   * The pattern may contain two special characters:<br>
   * '*' means zero or more characters<br>
   * '?' means one and only one character
   *
   * @param pattern The pattern to match against.
   *                Must not be <code>null</code>.
   * @param str     The string which must be matched against the pattern.
   *                Must not be <code>null</code>.
   * @param isCaseSensitive Whether or not matching should be performed
   *                        case sensitively.
   *
   *
   * @return <code>true</code> if the string matches against the pattern,
   *         or <code>false</code> otherwise.
   */
  public static boolean match(String pattern, String str,
                              boolean isCaseSensitive) {
    char[] patArr = pattern.toCharArray();
    char[] strArr = str.toCharArray();
    int patIdxStart = 0;
    int patIdxEnd = patArr.length - 1;
    int strIdxStart = 0;
    int strIdxEnd = strArr.length - 1;
    char ch;

    boolean containsStar = false;
    for (int i = 0; i < patArr.length; i++) {
      if (patArr[i] == '*') {
        containsStar = true;
        break;
      }
    }

    if (!containsStar) {
      // No '*'s, so we make a shortcut
      if (patIdxEnd != strIdxEnd) {
        return false; // Pattern and string do not have the same size
      }
      for (int i = 0; i <= patIdxEnd; i++) {
        ch = patArr[i];
        if (ch != '?') {
          if (isCaseSensitive && ch != strArr[i]) {
            return false; // Character mismatch
          }
          if (!isCaseSensitive && Character.toUpperCase(ch)
              != Character.toUpperCase(strArr[i])) {
            return false; // Character mismatch
          }
        }
      }
      return true; // String matches against pattern
    }

    if (patIdxEnd == 0) {
      return true; // Pattern contains only '*', which matches anything
    }

    // Process characters before first star
    while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
      if (ch != '?') {
        if (isCaseSensitive && ch != strArr[strIdxStart]) {
          return false; // Character mismatch
        }
        if (!isCaseSensitive && Character.toUpperCase(ch)
            != Character.toUpperCase(strArr[strIdxStart])) {
          return false; // Character mismatch
        }
      }
      patIdxStart++;
      strIdxStart++;
    }
    if (strIdxStart > strIdxEnd) {
      // All characters in the string are used. Check if only '*'s are
      // left in the pattern. If so, we succeeded. Otherwise failure.
      for (int i = patIdxStart; i <= patIdxEnd; i++) {
        if (patArr[i] != '*') {
          return false;
        }
      }
      return true;
    }

    // Process characters after last star
    while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
      if (ch != '?') {
        if (isCaseSensitive && ch != strArr[strIdxEnd]) {
          return false; // Character mismatch
        }
        if (!isCaseSensitive && Character.toUpperCase(ch)
            != Character.toUpperCase(strArr[strIdxEnd])) {
          return false; // Character mismatch
        }
      }
      patIdxEnd--;
      strIdxEnd--;
    }
    if (strIdxStart > strIdxEnd) {
      // All characters in the string are used. Check if only '*'s are
      // left in the pattern. If so, we succeeded. Otherwise failure.
      for (int i = patIdxStart; i <= patIdxEnd; i++) {
        if (patArr[i] != '*') {
          return false;
        }
      }
      return true;
    }

    // process pattern between stars. padIdxStart and patIdxEnd point
    // always to a '*'.
    while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
      int patIdxTmp = -1;
      for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
        if (patArr[i] == '*') {
          patIdxTmp = i;
          break;
        }
      }
      if (patIdxTmp == patIdxStart + 1) {
        // Two stars next to each other, skip the first one.
        patIdxStart++;
        continue;
      }
      // Find the pattern between padIdxStart & padIdxTmp in str between
      // strIdxStart & strIdxEnd
      int patLength = (patIdxTmp - patIdxStart - 1);
      int strLength = (strIdxEnd - strIdxStart + 1);
      int foundIdx = -1;
      strLoop:
          for (int i = 0; i <= strLength - patLength; i++) {
        for (int j = 0; j < patLength; j++) {
          ch = patArr[patIdxStart + j + 1];
          if (ch != '?') {
            if (isCaseSensitive && ch != strArr[strIdxStart + i
                + j]) {
              continue strLoop;
            }
            if (!isCaseSensitive
                && Character.toUpperCase(ch)
                != Character.toUpperCase(strArr[strIdxStart + i + j])) {
              continue strLoop;
            }
          }
        }

        foundIdx = strIdxStart + i;
        break;
      }

      if (foundIdx == -1) {
        return false;
      }

      patIdxStart = patIdxTmp;
      strIdxStart = foundIdx + patLength;
    }

    // All characters in the string are used. Check if only '*'s are left
    // in the pattern. If so, we succeeded. Otherwise failure.
    for (int i = patIdxStart; i <= patIdxEnd; i++) {
      if (patArr[i] != '*') {
        return false;
      }
    }
    return true;
  }

  /**
   * Breaks a path up into a Vector of path elements, tokenizing on
   * <code>File.separator</code>.
   *
   * @param path Path to tokenize. Must not be <code>null</code>.
   *
   * @return a Vector of path elements from the tokenized path
   */
  public static Vector<String> tokenizePath(String path) { //NOPMD
    return tokenizePath(path, separator);
  }

  /**
   * Breaks a path up into a Vector of path elements.  It tokenizes on
   *
   * @param path Path to tokenize. Must not be <code>null</code>.
   * @param separator the separator against which to tokenize.
   *
   * @return a Vector of path elements from the tokenized path
   * @since Ant 1.6
   */
  public static Vector<String> tokenizePath(String path, String separator) {  //NOPMD
    Vector<String> ret = new Vector<String>();
    StringTokenizer st = new StringTokenizer(path, separator);
    while (st.hasMoreTokens()) {
      ret.addElement(st.nextToken());
    }
    return ret;
  }

  /**
   * Same as {@link #tokenizePath tokenizePath} but hopefully faster.
   *
   * @param path Path to tokenize. Must not be <code>null</code>.
   * @return An array of path elements from the tokenized path.
   */
  private static String[] tokenizePathAsArray(String path) {
    char sep = separatorChar;
    int start = 0;
    int len = path.length();
    int count = 0;
    for (int pos = 0; pos < len; pos++) {
      if (path.charAt(pos) == sep) {
        if (pos != start) {
          count++;
        }
        start = pos + 1;
      }
    }
    if (len != start) {
      count++;
    }
    String[] l = new String[count];
    count = 0;
    start = 0;
    for (int pos = 0; pos < len; pos++) {
      if (path.charAt(pos) == sep) {
        if (pos != start) {
          String tok = path.substring(start, pos);
          l[count++] = tok;
        }
        start = pos + 1;
      }
    }
    if (len != start) {
      String tok = path.substring(start);
      l[count /*++*/] = tok;
    }
    return l;
  }

  /**
   * Returns dependency information on these two files. If src has been
   * modified later than target, it returns true. If target doesn't exist,
   * it likewise returns true. Otherwise, target is newer than src and
   * is not out of date, thus the method returns false. It also returns
   * false if the src file doesn't even exist, since how could the
   * target then be out of date.
   *
   * @param src the original file
   * @param target the file being compared against
   * @param granularity the amount in seconds of slack we will give in
   *        determining out of dateness
   * @return whether the target is out of date
   */
  public static boolean isOutOfDate(File src, File target, int granularity) {
    if (!src.exists()) {
      return false;
    }
    if (!target.exists()) {
      return true;
    }
    if ((src.lastModified() - granularity) > target.lastModified()) {
      return true;
    }
    return false;
  }

//  /**
//   * Returns dependency information on these two resources. If src has been
//   * modified later than target, it returns true. If target doesn't exist,
//   * it likewise returns true. Otherwise, target is newer than src and
//   * is not out of date, thus the method returns false. It also returns
//   * false if the src file doesn't even exist, since how could the
//   * target then be out of date.
//   *
//   * @param src the original resource
//   * @param target the resource being compared against
//   * @param granularity the amount in seconds of slack we will give in
//   *        determining out of dateness
//   * @return whether the target is out of date
//   */
//    public static boolean isOutOfDate(Resource src, Resource target,
//                                      int granularity) {
//        if (!src.isExists()) {
//            return false;
//        }
//        if (!target.isExists()) {
//            return true;
//        }
//        if ((src.getLastModified() - granularity) > target.getLastModified()) {
//            return true;
//        }
//        return false;
//    }

  /**
   * "Flattens" a string by removing all whitespace (space, tab, linefeed,
   * carriage return, and formfeed). This uses StringTokenizer and the
   * default set of tokens as documented in the single arguement constructor.
   *
   * @param input a String to remove all whitespace.
   * @return a String that has had all whitespace removed.
   */
  public static String removeWhitespace(String input) {
    StringBuffer result = new StringBuffer();
    if (input != null) {
      StringTokenizer st = new StringTokenizer(input);
      while (st.hasMoreTokens()) {
        result.append(st.nextToken());
      }
    }
    return result.toString();
  }

  /**
   * Tests if a string contains stars or question marks.
   * @param input a String which one wants to test for containing wildcard
   * @return true if the string contains at least a star or a question mark
   */
  public static boolean hasWildcards(String input) {
    return (input.indexOf('*') != -1 || input.indexOf('?') != -1);
  }

  /**
   * Removes from a pattern all tokens to the right containing wildcards.
   * @param input the input string
   * @return the leftmost part of the pattern without wildcards
   */
  public static String rtrimWildcardTokens(String input) {
    Vector<String> v = tokenizePath(input, separator);
    StringBuffer sb = new StringBuffer();
    for (int counter = 0; counter < v.size(); counter++) {
      if (hasWildcards(v.elementAt(counter))) {
        break;
      }
      if (counter > 0) {
        sb.append(separator);
      }
      sb.append(v.elementAt(counter));
    }
    return sb.toString();
  }
}
